﻿using Mono.Cecil;
using Mono.Cecil.Cil;
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using MethodAttributes = Mono.Cecil.MethodAttributes;

namespace BepInEx.Patcher
{
	class Program
	{
		static void WriteError(string message)
		{
			Console.ForegroundColor = ConsoleColor.Red;
			Console.WriteLine("Failed");
			Console.ResetColor();
			Console.WriteLine(message);
			Console.WriteLine();
		}

		static void WriteSuccess()
		{
			Console.ForegroundColor = ConsoleColor.Green;
			Console.WriteLine("Success");
			Console.ResetColor();
		}

		static string GetUnityEngineAssembly(string managedDir)
		{
			var path = Path.Combine(managedDir, "UnityEngine.CoreModule.dll");
			if (File.Exists(path))
				return path;
			return Path.Combine(managedDir, "UnityEngine.dll");
		}

		static void Main(string[] args)
		{
			Console.WriteLine($"BepInEx Patcher v{Assembly.GetExecutingAssembly().GetName().Version}");

			if (args.Length >= 1) //short circuit for specific dll patch
				Environment.Exit(PatchUnityExe(Path.GetDirectoryName(args[0]), args[0], out string message) ? 0 : 9999);

			bool hasFound = false;
			bool hasFailure = false;
			int patchCount = 0;

			foreach (string exePath in Directory.GetFiles(Directory.GetCurrentDirectory()))
			{
				string gameName = Path.GetFileNameWithoutExtension(exePath);

				string managedDir = Environment.CurrentDirectory + $@"\{gameName}_Data\Managed";

				string unityOutputDLL = GetUnityEngineAssembly(managedDir);

				if (!Directory.Exists(managedDir) || !File.Exists(unityOutputDLL))
					continue;

				hasFound = true;

				Console.Write($"Patching {gameName}... ");

				if (PatchUnityExe(managedDir, unityOutputDLL, out string message))
				{
					WriteSuccess();
					patchCount++;
				}
				else
				{
					WriteError(message);
					hasFailure = true;
				}
			}

			Console.WriteLine();

			if (!hasFound)
				Console.WriteLine("Didn't find any games to patch! Exiting.");
			else
				Console.WriteLine($"Patched {patchCount} assemblies!");

			if (hasFailure)
			{
				Console.WriteLine("Press any key to continue...");
				Console.ReadKey();
			}
			else
				System.Threading.Thread.Sleep(3000);
		}

		static bool PatchUnityExe(string managedDir, string unityOutputDLL, out string message)
		{
			message = null;

			try
			{
				string injectedDLL = Path.GetFullPath($"{managedDir}\\BepInEx.Bootstrap.dll");
				File.WriteAllBytes(injectedDLL, EmbeddedResource.Get("BepInEx.Patcher.BepInEx.Bootstrap.dll"));

				var defaultResolver = new DefaultAssemblyResolver();
				defaultResolver.AddSearchDirectory(managedDir);
				var rp = new ReaderParameters
				{
					AssemblyResolver = defaultResolver
				};

				string unityBackupDLL = $"{GetUnityEngineAssembly(managedDir)}.bak";

				//determine which assembly to use as a base
				AssemblyDefinition unity = AssemblyDefinition.ReadAssembly(unityOutputDLL, rp);

				if (!VerifyAssembly(unity, out message))
				{
					//try and fall back to .bak if exists
					if (File.Exists(unityBackupDLL))
					{
						unity.Dispose();
						unity = AssemblyDefinition.ReadAssembly(unityBackupDLL, rp);

						if (!VerifyAssembly(unity, out message))
						{
							//can't use anything
							unity.Dispose();
							message += "\r\nThe backup is not usable.";
							return false;
						}
					}
					else
					{
						//can't use anything
						unity.Dispose();
						message += "\r\nNo backup exists.";
						return false;
					}
				}
				else
				{
					//make a backup of the assembly
					File.Copy(unityOutputDLL, unityBackupDLL, true);
					unity.Dispose();
					unity = AssemblyDefinition.ReadAssembly(unityBackupDLL, rp);
				}

				//patch
				using (unity)
				using (AssemblyDefinition injected = AssemblyDefinition.ReadAssembly(injectedDLL, rp))
				{
					InjectAssembly(unity, injected);

					unity.Write(unityOutputDLL);
				}
			}
			catch (Exception e)
			{
				message = e.ToString();
				return false;
			}

			return true;
		}

		static void InjectAssembly(AssemblyDefinition unity, AssemblyDefinition injected)
		{
			//Entry point
			var originalInjectMethod = injected.MainModule.Types.First(x => x.Name == "Entrypoint")
											   .Methods.First(x => x.Name == "Init");

			var injectMethod = unity.MainModule.ImportReference(originalInjectMethod);

			var sceneManager = unity.MainModule.Types.First(x => x.Name == "Application");

			var voidType = unity.MainModule.ImportReference(typeof(void));
			var cctor = new MethodDefinition(".cctor",
				MethodAttributes.Static
				| MethodAttributes.Private
				| MethodAttributes.HideBySig
				| MethodAttributes.SpecialName
				| MethodAttributes.RTSpecialName,
				voidType);

			var ilp = cctor.Body.GetILProcessor();
			ilp.Append(ilp.Create(OpCodes.Call, injectMethod));
			ilp.Append(ilp.Create(OpCodes.Ret));

			sceneManager.Methods.Add(cctor);
		}

		static bool VerifyAssembly(AssemblyDefinition unity, out string message)
		{
			bool canPatch = true;
			message = "";

			//check if already patched
			if (unity.MainModule.AssemblyReferences.Any(x => x.Name.Contains("BepInEx")))
			{
				canPatch = false;

				message += "This assembly has already been patched by BepInEx.\r\n";
			}

			message = message.Trim();
			return canPatch;
		}
	}
}